/*
 * Copyright (c) 2014, Peter Thorson. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the WebSocket++ Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL PETER THORSON BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

#ifndef HTTP_PARSER_HPP
#define HTTP_PARSER_HPP

#include <algorithm>
#include <map>
#include <string>
#include <utility>

#include <websocketpp/utilities.hpp>
#include <websocketpp/http/constants.hpp>

namespace websocketpp
{
namespace http
{
namespace parser
{

namespace state
{
enum value {
  method,
  resource,
  version,
  headers
};
}

namespace body_encoding
{
enum value {
  unknown,
  plain,
  chunked
};
}

typedef std::map<std::string, std::string, utility::ci_less > header_list;

/// Read and return the next token in the stream
/**
 * Read until a non-token character is found and then return the token and
 * iterator to the next character to read
 *
 * @param begin An iterator to the beginning of the sequence
 * @param end An iterator to the end of the sequence
 * @return A pair containing the token and an iterator to the next character in
 * the stream
 */
template <typename InputIterator>
std::pair<std::string, InputIterator> extract_token (InputIterator begin,
    InputIterator end)
{
  InputIterator it = std::find_if (begin, end, &is_not_token_char);
  return std::make_pair (std::string (begin, it), it);
}

/// Read and return the next quoted string in the stream
/**
 * Read a double quoted string starting at `begin`. The quotes themselves are
 * stripped. The quoted value is returned along with an iterator to the next
 * character to read
 *
 * @param begin An iterator to the beginning of the sequence
 * @param end An iterator to the end of the sequence
 * @return A pair containing the string read and an iterator to the next
 * character in the stream
 */
template <typename InputIterator>
std::pair<std::string, InputIterator> extract_quoted_string (
  InputIterator begin,
  InputIterator end)
{
  std::string s;

  if (end == begin) {
    return std::make_pair (s, begin);
  }

  if (*begin != '"') {
    return std::make_pair (s, begin);
  }

  InputIterator cursor = begin + 1;
  InputIterator marker = cursor;

  cursor = std::find (cursor, end, '"');

  while (cursor != end) {
    // either this is the end or a quoted string
    if (* (cursor - 1) == '\\') {
      s.append (marker, cursor - 1);
      s.append (1, '"');
      ++cursor;
      marker = cursor;
    } else {
      s.append (marker, cursor);
      ++cursor;
      return std::make_pair (s, cursor);
    }

    cursor = std::find (cursor, end, '"');
  }

  return std::make_pair ("", begin);
}

/// Read and discard one unit of linear whitespace
/**
 * Read one unit of linear white space and return the iterator to the character
 * afterwards. If `begin` is returned, no whitespace was extracted.
 *
 * @param begin An iterator to the beginning of the sequence
 * @param end An iterator to the end of the sequence
 * @return An iterator to the character after the linear whitespace read
 */
template <typename InputIterator>
InputIterator extract_lws (InputIterator begin, InputIterator end)
{
  InputIterator it = begin;

  // strip leading CRLF
  if (end - begin > 2 && *begin == '\r' && * (begin + 1) == '\n' &&
      is_whitespace_char (static_cast<unsigned char> (* (begin + 2) ) ) ) {
    it += 3;
  }

  it = std::find_if (it, end, &is_not_whitespace_char);
  return it;
}

/// Read and discard linear whitespace
/**
 * Read linear white space until a non-lws character is read and return an
 * iterator to that character. If `begin` is returned, no whitespace was
 * extracted.
 *
 * @param begin An iterator to the beginning of the sequence
 * @param end An iterator to the end of the sequence
 * @return An iterator to the character after the linear whitespace read
 */
template <typename InputIterator>
InputIterator extract_all_lws (InputIterator begin, InputIterator end)
{
  InputIterator old_it;
  InputIterator new_it = begin;

  do {
    // Pull value from previous iteration
    old_it = new_it;

    // look ahead another pass
    new_it = extract_lws (old_it, end);
  } while (new_it != end && old_it != new_it);

  return new_it;
}

/// Extract HTTP attributes
/**
 * An http attributes list is a semicolon delimited list of key value pairs in
 * the format: *( ";" attribute "=" value ) where attribute is a token and value
 * is a token or quoted string.
 *
 * Attributes extracted are appended to the supplied attributes list
 * `attributes`.
 *
 * @param [in] begin An iterator to the beginning of the sequence
 * @param [in] end An iterator to the end of the sequence
 * @param [out] attributes A reference to the attributes list to append
 * attribute/value pairs extracted to
 * @return An iterator to the character after the last atribute read
 */
template <typename InputIterator>
InputIterator extract_attributes (InputIterator begin, InputIterator end,
                                  attribute_list &attributes)
{
  InputIterator cursor;
  bool first = true;

  if (begin == end) {
    return begin;
  }

  cursor = begin;
  std::pair<std::string, InputIterator> ret;

  while (cursor != end) {
    std::string name;

    cursor = http::parser::extract_all_lws (cursor, end);

    if (cursor == end) {
      break;
    }

    if (first) {
      // ignore this check for the very first pass
      first = false;
    } else {
      if (*cursor == ';') {
        // advance past the ';'
        ++cursor;
      } else {
        // non-semicolon in this position indicates end end of the
        // attribute list, break and return.
        break;
      }
    }

    cursor = http::parser::extract_all_lws (cursor, end);
    ret = http::parser::extract_token (cursor, end);

    if (ret.first.empty() ) {
      // error: expected a token
      return begin;
    } else {
      name = ret.first;
      cursor = ret.second;
    }

    cursor = http::parser::extract_all_lws (cursor, end);

    if (cursor == end || *cursor != '=') {
      // if there is an equals sign, read the attribute value. Otherwise
      // record a blank value and continue
      attributes[name].clear();
      continue;
    }

    // advance past the '='
    ++cursor;

    cursor = http::parser::extract_all_lws (cursor, end);

    if (cursor == end) {
      // error: expected a token or quoted string
      return begin;
    }

    ret = http::parser::extract_quoted_string (cursor, end);

    if (ret.second != cursor) {
      attributes[name] = ret.first;
      cursor = ret.second;
      continue;
    }

    ret = http::parser::extract_token (cursor, end);

    if (ret.first.empty() ) {
      // error : expected token or quoted string
      return begin;
    } else {
      attributes[name] = ret.first;
      cursor = ret.second;
    }
  }

  return cursor;
}

/// Extract HTTP parameters
/**
 * An http parameters list is a comma delimited list of tokens followed by
 * optional semicolon delimited attributes lists.
 *
 * Parameters extracted are appended to the supplied parameters list
 * `parameters`.
 *
 * @param [in] begin An iterator to the beginning of the sequence
 * @param [in] end An iterator to the end of the sequence
 * @param [out] parameters A reference to the parameters list to append
 * paramter values extracted to
 * @return An iterator to the character after the last parameter read
 */
template <typename InputIterator>
InputIterator extract_parameters (InputIterator begin, InputIterator end,
                                  parameter_list &parameters)
{
  InputIterator cursor;

  if (begin == end) {
    // error: expected non-zero length range
    return begin;
  }

  cursor = begin;
  std::pair<std::string, InputIterator> ret;

  /**
   * LWS
   * token
   * LWS
   * *(";" method-param)
   * LWS
   * ,=loop again
   */
  while (cursor != end) {
    std::string parameter_name;
    attribute_list attributes;

    // extract any stray whitespace
    cursor = http::parser::extract_all_lws (cursor, end);

    if (cursor == end) {
      break;
    }

    ret = http::parser::extract_token (cursor, end);

    if (ret.first.empty() ) {
      // error: expected a token
      return begin;
    } else {
      parameter_name = ret.first;
      cursor = ret.second;
    }

    // Safe break point, insert parameter with blank attributes and exit
    cursor = http::parser::extract_all_lws (cursor, end);

    if (cursor == end) {
      //parameters[parameter_name] = attributes;
      parameters.push_back (std::make_pair (parameter_name, attributes) );
      break;
    }

    // If there is an attribute list, read it in
    if (*cursor == ';') {
      InputIterator acursor;

      ++cursor;
      acursor = http::parser::extract_attributes (cursor, end, attributes);

      if (acursor == cursor) {
        // attribute extraction ended in syntax error
        return begin;
      }

      cursor = acursor;
    }

    // insert parameter into output list
    //parameters[parameter_name] = attributes;
    parameters.push_back (std::make_pair (parameter_name, attributes) );

    cursor = http::parser::extract_all_lws (cursor, end);

    if (cursor == end) {
      break;
    }

    // if next char is ',' then read another parameter, else stop
    if (*cursor != ',') {
      break;
    }

    // advance past comma
    ++cursor;

    if (cursor == end) {
      // expected more bytes after a comma
      return begin;
    }
  }

  return cursor;
}

inline std::string strip_lws (std::string const &input)
{
  std::string::const_iterator begin = extract_all_lws (input.begin(),
                                      input.end() );

  if (begin == input.end() ) {
    return std::string();
  }

  std::string::const_reverse_iterator rbegin = extract_all_lws (input.rbegin(),
      input.rend() );

  if (rbegin == input.rend() ) {
    return std::string();
  }

  return std::string (begin, rbegin.base() );
}

/// Base HTTP parser
/**
 * Includes methods and data elements common to all types of HTTP messages such
 * as headers, versions, bodies, etc.
 */
class parser
{
public:
  parser()
    : m_header_bytes (0)
    , m_body_bytes_needed (0)
    , m_body_bytes_max (max_body_size)
    , m_body_encoding (body_encoding::unknown) {}

  /// Get the HTTP version string
  /**
   * @return The version string for this parser
   */
  std::string const &get_version() const
  {
    return m_version;
  }

  /// Set HTTP parser Version
  /**
   * Input should be in format: HTTP/x.y where x and y are positive integers.
   * @todo Does this method need any validation?
   *
   * @param [in] version The value to set the HTTP version to.
   */
  void set_version (std::string const &version);

  /// Get the value of an HTTP header
  /**
   * @todo Make this method case insensitive.
   *
   * @param [in] key The name/key of the header to get.
   * @return The value associated with the given HTTP header key.
   */
  std::string const &get_header (std::string const &key) const;

  /// Extract an HTTP parameter list from a parser header.
  /**
   * If the header requested doesn't exist or exists and is empty the
   * parameter list is valid (but empty).
   *
   * @param [in] key The name/key of the HTTP header to use as input.
   * @param [out] out The parameter list to store extracted parameters in.
   * @return Whether or not the input was a valid parameter list.
   */
  bool get_header_as_plist (std::string const &key, parameter_list &out)
  const;

  /// Append a value to an existing HTTP header
  /**
   * This method will set the value of the HTTP header `key` with the
   * indicated value. If a header with the name `key` already exists, `val`
   * will be appended to the existing value.
   *
   * @todo Make this method case insensitive.
   * @todo Should there be any restrictions on which keys are allowed?
   * @todo Exception free varient
   *
   * @see replace_header
   *
   * @param [in] key The name/key of the header to append to.
   * @param [in] val The value to append.
   */
  void append_header (std::string const &key, std::string const &val);

  /// Set a value for an HTTP header, replacing an existing value
  /**
   * This method will set the value of the HTTP header `key` with the
   * indicated value. If a header with the name `key` already exists, `val`
   * will replace the existing value.
   *
   * @todo Make this method case insensitive.
   * @todo Should there be any restrictions on which keys are allowed?
   * @todo Exception free varient
   *
   * @see append_header
   *
   * @param [in] key The name/key of the header to append to.
   * @param [in] val The value to append.
   */
  void replace_header (std::string const &key, std::string const &val);

  /// Remove a header from the parser
  /**
   * Removes the header entirely from the parser. This is different than
   * setting the value of the header to blank.
   *
   * @todo Make this method case insensitive.
   *
   * @param [in] key The name/key of the header to remove.
   */
  void remove_header (std::string const &key);

  /// Get HTTP body
  /**
   * Gets the body of the HTTP object
   *
   * @return The body of the HTTP message.
   */
  std::string const &get_body() const
  {
    return m_body;
  }

  /// Set body content
  /**
   * Set the body content of the HTTP response to the parameter string. Note
   * set_body will also set the Content-Length HTTP header to the appropriate
   * value. If you want the Content-Length header to be something else, do so
   * via replace_header("Content-Length") after calling set_body()
   *
   * @param value String data to include as the body content.
   */
  void set_body (std::string const &value);

  /// Get body size limit
  /**
   * Retrieves the maximum number of bytes to parse & buffer before canceling
   * a request.
   *
   * @since 0.5.0
   *
   * @return The maximum length of a message body.
   */
  size_t get_max_body_size() const
  {
    return m_body_bytes_max;
  }

  /// Set body size limit
  /**
   * Set the maximum number of bytes to parse and buffer before canceling a
   * request.
   *
   * @since 0.5.0
   *
   * @param value The size to set the max body length to.
   */
  void set_max_body_size (size_t value)
  {
    m_body_bytes_max = value;
  }

  /// Extract an HTTP parameter list from a string.
  /**
   * @param [in] in The input string.
   * @param [out] out The parameter list to store extracted parameters in.
   * @return Whether or not the input was a valid parameter list.
   */
  bool parse_parameter_list (std::string const &in, parameter_list &out)
  const;
protected:
  /// Process a header line
  /**
   * @todo Update this method to be exception free.
   *
   * @param [in] begin An iterator to the beginning of the sequence.
   * @param [in] end An iterator to the end of the sequence.
   */
  void process_header (std::string::iterator begin, std::string::iterator end);

  /// Prepare the parser to begin parsing body data
  /**
   * Inspects headers to determine if the message has a body that needs to be
   * read. If so, sets up the necessary state, otherwise returns false. If
   * this method returns true and loading the message body is desired call
   * `process_body` until it returns zero bytes or an error.
   *
   * Must not be called until after all headers have been processed.
   *
   * @since 0.5.0
   *
   * @return True if more bytes are needed to load the body, false otherwise.
   */
  bool prepare_body();

  /// Process body data
  /**
   * Parses body data.
   *
   * @since 0.5.0
   *
   * @param [in] begin An iterator to the beginning of the sequence.
   * @param [in] end An iterator to the end of the sequence.
   * @return The number of bytes processed
   */
  size_t process_body (char const *buf, size_t len);

  /// Check if the parser is done parsing the body
  /**
   * Behavior before a call to `prepare_body` is undefined.
   *
   * @since 0.5.0
   *
   * @return True if the message body has been completed loaded.
   */
  bool body_ready() const
  {
    return (m_body_bytes_needed == 0);
  }

  /// Generate and return the HTTP headers as a string
  /**
   * Each headers will be followed by the \r\n sequence including the last one.
   * A second \r\n sequence (blank header) is not appended by this method
   *
   * @return The HTTP headers as a string.
   */
  std::string raw_headers() const;

  std::string m_version;
  header_list m_headers;

  size_t                  m_header_bytes;

  std::string             m_body;
  size_t                  m_body_bytes_needed;
  size_t                  m_body_bytes_max;
  body_encoding::value    m_body_encoding;
};

} // namespace parser
} // namespace http
} // namespace websocketpp

#include <websocketpp/http/impl/parser.hpp>

#endif // HTTP_PARSER_HPP
